package com.cloudlead.common.persistence.impl;

import com.cloudlead.common.lang.BusinessModule;
import com.cloudlead.common.lang.Messages;
import com.cloudlead.common.lang.persistence.IdEntity;
import com.cloudlead.common.lang.persistence.StatisticsEntity;
import com.cloudlead.common.lang.security.SecurityUtils;
import com.cloudlead.common.persistence.*;
import com.mysema.query.BooleanBuilder;
import com.mysema.query.sql.dml.DefaultMapper;
import com.mysema.query.sql.dml.SQLInsertClause;
import com.mysema.query.sql.dml.SQLUpdateClause;
import org.joda.time.DateTime;
import org.springframework.transaction.annotation.Transactional;

import javax.sql.DataSource;
import javax.validation.groups.Default;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;

public abstract class AbstractCrudRepository<Pojo extends IdEntity<ID>, ID extends Serializable>
        extends AbstractMultiTableQueryDslRepository<Pojo, ID> implements
        CrudRepository<Pojo, ID> {

    public AbstractCrudRepository(BusinessModule module, DataSource dataSource) {
        super(module, dataSource);
    }

    public AbstractCrudRepository(BusinessModule module, QueryDslConfig queryDslConfig) {
        super(module, queryDslConfig);
    }

    @Override
    @Transactional
    public void save(Pojo entity) {
        ParamAssert.notNull(entity, "save(entity)", this.getClass());
        if (hasValidator()) {
            validator(entity);
        }
        try {
            if (entity instanceof StatisticsEntity) {
                StatisticsEntity statisticsEntity = (StatisticsEntity) entity;
                String username = SecurityUtils.getUsername();
                statisticsEntity.setCreateUser(username);
                statisticsEntity.setCreateTime(new DateTime());
            }
            ID executeWithKey = sqlQueryFactory.insert(root).populate(entity)
                    .executeWithKey(primaryKeyClass);
            if (null != executeWithKey) {
                entity.setId(executeWithKey);
            }
        } catch (Exception e) {
            e.printStackTrace();
            throw new DataAccessException(Messages.getMessage("persistence.save.error"), e);
        }
    }

    @Override
    @Transactional
    public void save(Collection<Pojo> entities) {
        if (null != entities && !entities.isEmpty()) {
            SQLInsertClause insertClause = sqlQueryFactory.insert(root);
            for (Pojo entity : entities) {
                if (hasValidator()) {
                    validator(entity);
                }
                insertClause.populate(entity).addBatch();
            }

            try {
                List<ID> executeWithKeys = insertClause
                        .executeWithKeys(primaryKeyClass);
                int index = 0;
                for (Pojo entity : entities) {
                    if (executeWithKeys.size() > index) {
                        ID id = executeWithKeys.get(index);
                        entity.setId(id);
                    }
                    index++;
                }

            } catch (Exception e) {
                e.printStackTrace();
                throw new DataAccessException(Messages.getMessage("persistence.batch.save.error"), e);
            }
        }
    }

    @Override
    @Transactional
    public void update(Pojo entity) {
        update(entity, true);
    }

    @Override
    @Transactional
    public void updateWithNotNull(Pojo entity) {
        update(entity, false);
    }

    private void update(Pojo entity, boolean withNullBindings) {
        ParamAssert.notNull(entity, "update(entity)", this.getClass());
        ParamAssert.notNull(entity.getId(), "update(entity > id)",
                this.getClass());

        if (withNullBindings && hasValidator()) {
            validator(entity, Default.class, UpdateCheck.class);
        }

        try {
            if (entity instanceof StatisticsEntity) {
                StatisticsEntity statisticsEntity = (StatisticsEntity) entity;
                String username = SecurityUtils.getUsername();
                statisticsEntity.setUpdateUser(username);
                statisticsEntity.setUpdateTime(new DateTime());
            }
            sqlQueryFactory.update(root)
                    .populate(
                            entity,
                            withNullBindings ? DefaultMapper.WITH_NULL_BINDINGS
                                    : DefaultMapper.DEFAULT)
                    .where(builder.get(ID).eq(entity.getId())).execute();
        } catch (Exception e) {
            e.printStackTrace();
            throw new DataAccessException(Messages.getMessage("persistence.update.error"), e);
        }
    }

    @Override
    @Transactional
    public void update(Collection<Pojo> entities) {
        update(entities, true);
    }

    @Override
    @Transactional
    public void update(Collection<Pojo> entities, boolean withNullBindings) {
        if (null != entities && !entities.isEmpty()) {
            SQLUpdateClause updateClause = sqlQueryFactory.update(root);
            for (Pojo entity : entities) {
                ParamAssert.notNull(entity, "update(entities > entity)",
                        this.getClass());
                ParamAssert.notNull(entity.getId(),
                        "update(entities > entity > id)", this.getClass());
                if (withNullBindings && hasValidator()) {
                    validator(entity, Default.class, UpdateCheck.class);
                }
                updateClause
                        .populate(
                                entity,
                                withNullBindings ? DefaultMapper.WITH_NULL_BINDINGS
                                        : DefaultMapper.DEFAULT)
                        .where(builder.get(ID).eq(entity.getId())).addBatch();
            }
            try {
                updateClause.execute();
            } catch (Exception e) {
                e.printStackTrace();
                throw new DataAccessException(Messages.getMessage("persistence.batch.update.error"), e);
            }
        }

    }

    @Override
    @Transactional
    public void merge(Pojo entity) {
        ParamAssert.notNull(entity, "merge(entity)", this.getClass());
        if (null == entity.getId()) {
            save(entity);
        } else {
            update(entity, true);
        }

    }

    @Override
    @Transactional
    public void merge(Collection<Pojo> entities) {
        if (null != entities && !entities.isEmpty()) {
            Collection<Pojo> toSaveEntities = new ArrayList<>();
            Collection<Pojo> toUpdateEntities = new ArrayList<>();
            for (Pojo entity : entities) {
                if (null == entity.getId()) {
                    toSaveEntities.add(entity);
                } else {
                    toUpdateEntities.add(entity);
                }
            }

            if (toSaveEntities.size() > 0) {
                save(toSaveEntities);
            }
            if (toUpdateEntities.size() > 0) {
                update(toUpdateEntities, true);
            }
        }
    }

    @Override
    @Transactional
    public void merge(Collection<Pojo> newEntities, Collection<Pojo> oldEntities) {
        if (null != newEntities && !newEntities.isEmpty()) {
            Collection<Pojo> toSaveEntities = new ArrayList<>();
            Collection<Pojo> toUpdateEntities = new ArrayList<>();
            for (Pojo entity : newEntities) {
                if (null == entity.getId()) {
                    toSaveEntities.add(entity);
                } else {
                    toUpdateEntities.add(entity);
                }
            }

            if (toSaveEntities.size() > 0) {
                save(toSaveEntities);
            }
            if (toUpdateEntities.size() > 0) {
                update(toUpdateEntities, true);
            }

            Collection<Pojo> toDeleteEntities = new ArrayList<>();
            if (null != oldEntities && !oldEntities.isEmpty()) {
                for (Pojo pojo : oldEntities) {
                    if (!newEntities.contains(pojo)) {
                        toDeleteEntities.add(pojo);
                    }
                }
            }
            if (toDeleteEntities.size() > 0) {
                delete(toDeleteEntities);
            }
        } else if (null != oldEntities && !oldEntities.isEmpty()) {
            delete(oldEntities);
        }

    }

    @Override
    @Transactional(readOnly = true)
    public Pojo findById(ID id) {
        ParamAssert.notNull(id, "findById(id)", this.getClass());

        return findOne(builder.get(ID).eq(id));
    }

    @Override
    @Transactional(readOnly = true)
    public boolean exists(ID id) {
        ParamAssert.notNull(id, "exists(id)", this.getClass());

        try {
            long count = sqlQueryFactory.from(root)
                    .where(builder.get(ID).eq(id)).count();
            return count == 1;
        } catch (Exception e) {
            e.printStackTrace();
            throw new DataAccessException(Messages.getMessage("persistence.exists.error"), e);
        }
    }

    @Override
    @Transactional(readOnly = true)
    public List<Pojo> findAll() {
        return findAll(null);
    }

    @Override
    @Transactional(readOnly = true)
    public List<Pojo> findByIds(Collection<ID> ids) {
        if (null != ids) {
            BooleanBuilder booleanBuilder = new BooleanBuilder();
            for (ID id : ids) {
                booleanBuilder.or(builder.get(ID).eq(id));
            }
            try {
                return sqlQueryFactory.from(root)
                        .where(booleanBuilder.getValue()).list(root);
            } catch (Exception e) {
                e.printStackTrace();
                throw new DataAccessException(Messages.getMessage("persistence.findByIds.error"), e);
            }
        }

        return Collections.emptyList();
    }

    @Override
    @Transactional(readOnly = true)
    public long count() {
        return count(null);
    }

    @Override
    @Transactional
    public void deleteById(ID id) {
        ParamAssert.notNull(id, "deleteById(id)", this.getClass());

        ParamAssert.isTrue(exists(id), String.format(
                        "No %s entity with id %s exists!", entityClass, id),
                "deleteById(id)", this.getClass());

        deleteAll(builder.get(ID).eq(id));
    }

    @Override
    @Transactional
    public void deleteByIds(Collection<ID> ids) {
        if (null != ids) {
            BooleanBuilder booleanBuilder = new BooleanBuilder();
            for (ID id : ids) {
                booleanBuilder.or(builder.get(ID).eq(id));
            }
            deleteAll(booleanBuilder.getValue());
        }

    }

    @Override
    @Transactional
    public void delete(Pojo entity) {
        ParamAssert.notNull(entity, "delete(entity)", this.getClass());
        ParamAssert.notNull(entity.getId(), "delete (entity)", this.getClass());

        deleteById(entity.getId());
    }

    @Override
    @Transactional
    public void delete(Collection<? extends Pojo> entities) {
        if (null != entities && !entities.isEmpty()) {
            Collection<ID> ids = new ArrayList<>();
            for (Pojo entity : entities) {
                ParamAssert.notNull(entity, "delete(entities > entity)",
                        this.getClass());
                ParamAssert.notNull(entity.getId(),
                        "delete (entities > entity > id)", this.getClass());
                ids.add(entity.getId());
            }
            deleteByIds(ids);
        }
    }

}
